package spike.s004.fastmatch.multigradehash;

import javaslang.Tuple2;
import org.apache.log4j.Logger;
import org.springframework.util.CollectionUtils;
import spike.utils.CounterWheels;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by ylyang on 05/04/2017.
 */
public class MultiGradeHashMatcher<EntityType> {
    private Logger logger = Logger.getLogger(MultiGradeHashMatcher.class);

    private static final String NOT_APPLICABLE = "N/A" + "_" + new Date().getTime() + "";
    private static final int NOT_TRIM_VALUE = -1;
    private final Tuple2<EntityType, Integer> constTupleNonMinValue = new Tuple2<>(null, -1);

    private TreeNode rootOfTreeNode;
    private List<String> columnSequence;

    private boolean useTrimBoolean = true;
    private boolean showLogBoolean = true;

    private MultiGradeHashMatcher() {

    }

    public MultiGradeHashMatcher(List<Tuple2<EntityType, Map<String, String>>> nodeCodeTuples) {
        this(nodeCodeTuples, new ArrayList<>(), true, false);
    }

    public MultiGradeHashMatcher(List<Tuple2<EntityType, Map<String, String>>> nodeCodeTuples, List<String> primaryKeys) {
        this(nodeCodeTuples, primaryKeys, true, false);
    }

    public MultiGradeHashMatcher(List<Tuple2<EntityType, Map<String, String>>> nodeCodeTuples, List<String> primaryKeys, boolean useTrimBoolean, boolean showLogBoolean) {
        this.useTrimBoolean = useTrimBoolean;
        this.showLogBoolean = showLogBoolean;

        createRootMap(nodeCodeTuples, primaryKeys);
    }

    public Tuple2<EntityType, Integer> match(Map<String, String> mapToMatch) {
        if (CollectionUtils.isEmpty(mapToMatch)
                || null == rootOfTreeNode
                || CollectionUtils.isEmpty(columnSequence)) {
            throw new RuntimeException();
        }

        showLOG(0, "********************");
        showLOG(0, "***BEGIN:" + mapToMatch);

        Tuple2<EntityType, Integer> value = matchMapWithDFS(mapToMatch, rootOfTreeNode, 0, NOT_TRIM_VALUE);

        showLOG(0, "match return:" + value._1);
        showLOG(0, "match max vx:" + value._2);

        showLOG(0, "***RETURN:" + value);
        showLOG(0, "********************");

        return value;
    }

    private Tuple2<EntityType, Integer> matchMapWithDFS(Map<String, String> mapToMatch, TreeNode currentTreeNode, int stepIndex, int alphaMaxValue) {
        showLOG(stepIndex, "step: " + stepIndex);

        if (Objects.isNull(currentTreeNode)) {
            showLOG(stepIndex, "match NULL.");
            return constTupleNonMinValue;
        }

        if (stepIndex >= columnSequence.size()) {
            showLOG(stepIndex, "return TARGET:" + currentTreeNode.getLeafNode()._2);
            return new Tuple2<>(currentTreeNode.getLeafNode()._1, 0);
        }

        //when the left value not bigger than alpha value. trim.
        if (canBeTrim(stepIndex, alphaMaxValue)) {
            return constTupleNonMinValue;
        }

        String columnName = columnSequence.get(stepIndex);
        if (!mapToMatch.containsKey(columnName)) {
            showLOG(stepIndex, "map not contains key: " + columnName);
            return matchNAWithLoopDFS(mapToMatch, currentTreeNode, stepIndex);
        } else {
            Map<String, TreeNode> currentNodeMap = currentTreeNode.getNodeMap();

            //match value - vs - match N/A. choose the max value then return.
            Tuple2<EntityType, Integer> maxReturnValue = constTupleNonMinValue;

            TreeNode nextMatchNode = currentNodeMap.get(mapToMatch.get(columnName));

            if (Objects.nonNull(nextMatchNode)) {
                showLOG(stepIndex, "next to match value:" + "(" + columnName + "," + mapToMatch.get(columnName) + ")");
                Tuple2<EntityType, Integer> valueA = matchMapWithDFS(mapToMatch, nextMatchNode, (1 + stepIndex), NOT_TRIM_VALUE);
                if (valueA._2 >= 0) {
                    maxReturnValue = new Tuple2<>(valueA._1, valueA._2 + 1);
                }
            }

            if (currentNodeMap.containsKey(NOT_APPLICABLE)) {
                showLOG(stepIndex, "next to match value:" + "(" + columnName + "," + "N/A" + ")");
                Tuple2<EntityType, Integer> valueB = matchMapWithDFS(mapToMatch, currentNodeMap.get(NOT_APPLICABLE), (1 + stepIndex), maxReturnValue._2);
                if (valueB._2 > maxReturnValue._2) {
                    maxReturnValue = valueB;
                }
            }

            return maxReturnValue;
        }
    }

    private boolean canBeTrim(int stepIndex, int aTrimValue) {
        if (useTrimBoolean) {
            if (aTrimValue == NOT_TRIM_VALUE) {
                return false;
            }

            if (columnSequence.size() - stepIndex <= aTrimValue) {
                showLOG(stepIndex, "can be trim.");
                return true;
            }
        }

        return false;
    }

    private Tuple2<EntityType, Integer> matchNAWithLoopDFS(Map<String, String> mapToMatch, TreeNode currentTreeNode, int stepIndex) {
        Tuple2<EntityType, Integer> maxReturnValue = constTupleNonMinValue;

        for (TreeNode nextTreeNode : currentTreeNode.nodeMap.values()) {
            showLOG(stepIndex, "recollection ->");
            Tuple2<EntityType, Integer> value = matchMapWithDFS(mapToMatch, nextTreeNode, (1 + stepIndex), maxReturnValue._2);
            //showLOG();
            if (Objects.nonNull(value) && value._2 > maxReturnValue._2) {
                maxReturnValue = value;
            }
            showLOG(stepIndex, "recollection <-");
        }

        return maxReturnValue;
    }

    private class TreeNode {
        private Map<String, TreeNode> nodeMap;
        private Tuple2<EntityType, Map<String, String>> leafNode;

        private TreeNode(boolean createMap) {
            if (createMap) {
                nodeMap = new HashMap<>();
            }
        }

        public Map<String, TreeNode> getNodeMap() {
            return nodeMap;
        }

        public Tuple2<EntityType, Map<String, String>> getLeafNode() {
            return leafNode;
        }

        public void setLeafNode(Tuple2<EntityType, Map<String, String>> leafNode) {
            this.leafNode = leafNode;
        }
    }

    private void createRootMap(List<Tuple2<EntityType, Map<String, String>>> nodeCodeTuples, List<String> primaryKeys) {
        checkInputs(nodeCodeTuples, primaryKeys);

        columnSequence = collectColumns(nodeCodeTuples, primaryKeys);

        rootOfTreeNode = new TreeNode(true);

        nodeCodeTuples.forEach(nodeCode -> {
            TreeNode currentTreeNode = rootOfTreeNode;

            for (String column : columnSequence) {
                String value = nodeCode._2.getOrDefault(column, NOT_APPLICABLE);

                Map<String, TreeNode> nodeMap = currentTreeNode.getNodeMap();
                if (!nodeMap.containsKey(value)) {
                    nodeMap.put(value, new TreeNode(true));
                }

                currentTreeNode = nodeMap.get(value);
            }

            currentTreeNode.setLeafNode(nodeCode);
        });
    }

    private void checkInputs(List<Tuple2<EntityType, Map<String, String>>> nodeCodeTuples, List<String> primaryKeys) {
        if (CollectionUtils.isEmpty(nodeCodeTuples)) {
            throw new RuntimeException();
        }

        for (Tuple2<EntityType, Map<String, String>> nodeCode : nodeCodeTuples) {
            if (CollectionUtils.isEmpty(nodeCode._2)) {
                throw new RuntimeException();
            }

            if (nodeCode._2.values().contains(null)) {
                throw new RuntimeException("NULL");
            }

            if (null == nodeCode._1) {
                throw new RuntimeException("NULL");
            }

            primaryKeys.forEach(pk -> {
                if (!nodeCode._2.containsKey(pk)) {
                    throw new RuntimeException("PK:" + pk);
                }
            });
        }
    }

    private List<String> collectColumns(List<Tuple2<EntityType, Map<String, String>>> nodeCodeTuples, List<String> primaryKeys) {
        CounterWheels<String> counterWheels = new CounterWheels();
        nodeCodeTuples.forEach(matchTuple -> {
            matchTuple._2.keySet().forEach(s -> {
                counterWheels.count(s);
            });
        });

        List<String> columns = counterWheels.orderByCountDesc().map(e -> e.getKey()).collect(Collectors.toList());

        return (new ArrayList<String>() {{
            if (!CollectionUtils.isEmpty(primaryKeys)) {
                addAll(primaryKeys);
            }

            addAll(columns);
        }}).stream().distinct().collect(Collectors.toList());
    }

    private void showLOG(int stepIndex, String message) {
        if (showLogBoolean) {
            StringBuffer m = new StringBuffer();
            for (int i = 0; i < stepIndex; i++) {
                m.append("    ");
            }

            m.append(message);

            logger.info(m);
        }
    }

    public void showTree() {
        showLOG(0, "***************************************");
        showLOG(0, columnSequence.toString());
        showMapWithDFS(0, rootOfTreeNode);
        showLOG(0, "***************************************");
    }

    private void showMapWithDFS(int stepIndex, TreeNode treeNode) {
        Tuple2<EntityType, Map<String, String>> leafNode = treeNode.getLeafNode();
        if (leafNode != null) {
            showLOG(stepIndex, leafNode.toString());
        } else {
            for (Map.Entry<String, TreeNode> entry : treeNode.nodeMap.entrySet()) {
                String code = String.format("(%s)%s", columnSequence.get(stepIndex), entry.getKey());
                showLOG(stepIndex, code);

                showMapWithDFS(stepIndex + 1, entry.getValue());
            }
        }
    }
}
